/*******************************************************************************
 * Copyright (c) 2003, 2005 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package com.architexa.org.eclipse.gef.ui.rulers;

import com.architexa.org.eclipse.draw2d.AbstractBorder;
import com.architexa.org.eclipse.draw2d.ColorConstants;
import com.architexa.org.eclipse.draw2d.DefaultRangeModel;
import com.architexa.org.eclipse.draw2d.FigureCanvas;
import com.architexa.org.eclipse.draw2d.Graphics;
import com.architexa.org.eclipse.draw2d.IFigure;
import com.architexa.org.eclipse.draw2d.PositionConstants;
import com.architexa.org.eclipse.draw2d.RangeModel;
import com.architexa.org.eclipse.draw2d.Viewport;
import com.architexa.org.eclipse.draw2d.geometry.Insets;
import com.architexa.org.eclipse.gef.DragTracker;
import com.architexa.org.eclipse.gef.EditDomain;
import com.architexa.org.eclipse.gef.EditPart;
import com.architexa.org.eclipse.gef.GraphicalEditPart;
import com.architexa.org.eclipse.gef.GraphicalViewer;
import com.architexa.org.eclipse.gef.Handle;
import com.architexa.org.eclipse.gef.RootEditPart;
import com.architexa.org.eclipse.gef.internal.ui.rulers.GuideEditPart;
import com.architexa.org.eclipse.gef.internal.ui.rulers.RulerContextMenuProvider;
import com.architexa.org.eclipse.gef.internal.ui.rulers.RulerEditPart;
import com.architexa.org.eclipse.gef.internal.ui.rulers.RulerEditPartFactory;
import com.architexa.org.eclipse.gef.internal.ui.rulers.RulerRootEditPart;
import com.architexa.org.eclipse.gef.rulers.RulerProvider;
import com.architexa.org.eclipse.gef.ui.parts.GraphicalViewerKeyHandler;
import com.architexa.org.eclipse.gef.ui.parts.ScrollingGraphicalViewer;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;

import org.eclipse.jface.util.Assert;



/**
 * A RulerComposite is used to show rulers to the north and west of the control of a
 * given {@link #setGraphicalViewer(ScrollingGraphicalViewer) graphical viewer}.  The
 * rulers will be shown based on whether or not 
 * {@link com.architexa.org.eclipse.gef.rulers.RulerProvider#PROPERTY_HORIZONTAL_RULER horizontal ruler} 
 * and {@link com.architexa.org.eclipse.gef.rulers.RulerProvider#PROPERTY_VERTICAL_RULER vertical ruler}
 * properties are set on the given viewer, and the value of the 
 * {@link com.architexa.org.eclipse.gef.rulers.RulerProvider#PROPERTY_RULER_VISIBILITY visibility} 
 * property.
 * 
 * @author Pratik Shah
 * @since 3.0
 */
public class RulerComposite
	extends Composite
{
	
private EditDomain rulerEditDomain;
private GraphicalViewer left, top;
private FigureCanvas editor;
private GraphicalViewer diagramViewer;
private Font font;
private Listener layoutListener;
private PropertyChangeListener propertyListener;
private boolean layingOut = false;
private boolean isRulerVisible = true;
private boolean needToLayout = false;
private Runnable runnable = new Runnable() {
	public void run() {
		layout(false);
	}
};

/**
 * Constructor
 * 
 * @param parent	a widget which will be the parent of the new instance (cannot be null)
 * @param style		the style of widget to construct
 * @see	Composite#Composite(org.eclipse.swt.widgets.Composite, int)
 */
public RulerComposite(Composite parent, int style) {
	super(parent, style);
	addDisposeListener(new DisposeListener() {
		public void widgetDisposed(DisposeEvent e) {
			disposeResources();
		}
	});
}

/*
 * Calculates the proper trim.  Includes scrollbars' sizes only if they're visible.
 */
private static Rectangle calculateEditorTrim(Canvas canvas) {
	/*
	 * Workaround for Bug# 87712
	 * Calculating the trim using the clientArea.
	 */
	Rectangle bounds = canvas.getBounds();
	Rectangle clientArea = canvas.getClientArea();
	Rectangle result = new Rectangle(
			0, 0, bounds.width - clientArea.width, bounds.height - clientArea.height);
	if (result.width != 0 || result.height != 0) {
		Rectangle trim = canvas.computeTrim(0, 0, 0, 0);
		result.x = result.height == 0 ? 0 : trim.x;
		result.y = result.width == 0 ? 0 : trim.y;
	}
	return result;
}

private static Rectangle calculateRulerTrim(Canvas canvas) {
	if ("carbon".equals(SWT.getPlatform())) { //$NON-NLS-1$
		Rectangle trim = canvas.computeTrim(0, 0, 0, 0);
		trim.width = 0 - trim.x * 2;
		trim.height = 0 - trim.y * 2;
		return trim;
	}
	return new Rectangle(0, 0, 0, 0);
}

private GraphicalViewer createRulerContainer(int orientation) {
	ScrollingGraphicalViewer viewer = new RulerViewer();
	final boolean isHorizontal = orientation == PositionConstants.NORTH 
			|| orientation == PositionConstants.SOUTH;

	// Finish initializing the viewer
	viewer.setRootEditPart(new RulerRootEditPart(isHorizontal));
	viewer.setEditPartFactory(new RulerEditPartFactory(diagramViewer));
	viewer.createControl(this);
	((GraphicalEditPart)viewer.getRootEditPart()).getFigure()
			.setBorder(new RulerBorder(isHorizontal));
	viewer.setProperty(GraphicalViewer.class.toString(), diagramViewer);
	
	// Configure the viewer's control
	FigureCanvas canvas = (FigureCanvas)viewer.getControl();
	canvas.setScrollBarVisibility(FigureCanvas.NEVER);
	if (font == null) {
		FontData[] data = canvas.getFont().getFontData();
		for (int i = 0; i < data.length; i++) {
			data[i].setHeight(data[i].getHeight() - 1);
		}
		font = new Font(Display.getCurrent(), data);
	}
	canvas.setFont(font);
	if (isHorizontal) {
		canvas.getViewport().setHorizontalRangeModel(
				editor.getViewport().getHorizontalRangeModel());
	} else {
		canvas.getViewport().setVerticalRangeModel(
				editor.getViewport().getVerticalRangeModel());
	}

	// Add the viewer to the rulerEditDomain
	if (rulerEditDomain == null) {
		rulerEditDomain = new EditDomain();
		rulerEditDomain.setCommandStack(diagramViewer.getEditDomain().getCommandStack());
	}
	rulerEditDomain.addViewer(viewer);
	
	return viewer;
}

private void disposeResources() {
	if (diagramViewer != null)
		diagramViewer.removePropertyChangeListener(propertyListener);
	if (font != null)
		font.dispose();
	// layoutListener is not being removed from the scroll bars because they are already
	// disposed at this point.
}

private void disposeRulerViewer(GraphicalViewer viewer) {
	if (viewer == null)
		return;
	/*
	 * There's a tie from the editor's range model to the RulerViewport (via a listener) 
	 * to the RulerRootEditPart to the RulerViewer.  Break this tie so that the viewer 
	 * doesn't leak and can be garbage collected.
	 */
	RangeModel rModel = new DefaultRangeModel();
	Viewport port = ((FigureCanvas)viewer.getControl()).getViewport();
	port.setHorizontalRangeModel(rModel);
	port.setVerticalRangeModel(rModel);
	rulerEditDomain.removeViewer(viewer);
	viewer.getControl().dispose();
}

private void doLayout() {
	if (left == null && top == null) {
		Rectangle area = getClientArea();
		if (!editor.getBounds().equals(area))
			editor.setBounds(area);
		return;
	}
	
	int leftWidth = 0, topHeight = 0;
	Rectangle leftTrim = null, topTrim = null;
	if (left != null) {
		leftTrim = calculateRulerTrim((Canvas)left.getControl());
		// Adding the trim width here because FigureCanvas#computeSize() does not
		leftWidth = left.getControl().computeSize(SWT.DEFAULT, SWT.DEFAULT).x 
				+ leftTrim.width;
	}
	if (top != null) {
		topTrim = calculateRulerTrim((Canvas)top.getControl());
		topHeight = top.getControl().computeSize(SWT.DEFAULT, SWT.DEFAULT).y
				+ topTrim.height;
	}

	Rectangle editorSize = getClientArea();
	editorSize.x = leftWidth;
	editorSize.y = topHeight;
	editorSize.width -= leftWidth;
	editorSize.height -= topHeight;
	editor.setBounds(editorSize);

	/*
	 * Fix for Bug# 67554
	 * Take trim into account.  Some platforms (such as MacOS and Motif) leave some 
	 * trimming around some canvasses.
	 */
	Rectangle trim = calculateEditorTrim(editor);
	if (left != null) {
		// The - 1 and + 1 are to compensate for the RulerBorder
		left.getControl().setBounds(0, topHeight - trim.x + leftTrim.x - 1, 
				leftWidth, editorSize.height - trim.height + leftTrim.height + 1);
	}
	if (top != null) {
		top.getControl().setBounds(leftWidth - trim.y + topTrim.y - 1, 0, 
				editorSize.width - trim.width + topTrim.width + 1, topHeight);
	}
}

private GraphicalViewer getRulerContainer(int orientation) {
	GraphicalViewer result = null;
	switch(orientation) {
		case PositionConstants.NORTH:
			result = top;
			break;
		case PositionConstants.WEST:
			result = left;
	}
	return result;
}

/**
 * @see org.eclipse.swt.widgets.Composite#layout(boolean)
 */
public void layout(boolean change) {
	if (!layingOut && !isDisposed()) {
		checkWidget();
		if (change || needToLayout) {
			needToLayout = false;
			layingOut = true;
			doLayout();
			layingOut = false;			
		}
	}
}

/**
 * Creates rulers for the given graphical viewer.
 * <p>
 * The primaryViewer or its Control cannot be <code>null</code>.  The primaryViewer's
 * Control should be a FigureCanvas and a child of this Composite.  This method should
 * only be invoked once.
 * <p>
 * To create ruler(s), simply add the RulerProvider(s) (with the right key: 
 * RulerProvider.PROPERTY_HORIZONTAL_RULER or RulerProvider.PROPERTY_VERTICAL_RULER) 
 * as a property on the given viewer.  It can be done after this method is invoked.
 * RulerProvider.PROPERTY_RULER_VISIBILITY can be used to show/hide the rulers.
 * 
 * @param	primaryViewer	The graphical viewer for which the rulers have to be created
 */
public void setGraphicalViewer(ScrollingGraphicalViewer primaryViewer) {
	// pre-conditions
	Assert.isNotNull(primaryViewer);
	Assert.isNotNull(primaryViewer.getControl());
	Assert.isTrue(diagramViewer == null);
	
	diagramViewer = primaryViewer;
	editor = (FigureCanvas)diagramViewer.getControl();

	// layout whenever the scrollbars are shown or hidden, and whenever the RulerComposite
	// is resized
	layoutListener = new Listener() {
		public void handleEvent(Event event) {
			// @TODO:Pratik  If you use Display.asyncExec(runnable) here, some flashing
			// occurs.  You can see it when the palette is in the editor, and you hit
			// the button to show/hide it.
			layout(true);
		}
	};
	addListener(SWT.Resize, layoutListener);
	editor.getHorizontalBar().addListener(SWT.Show, layoutListener);
	editor.getHorizontalBar().addListener(SWT.Hide, layoutListener);
	editor.getVerticalBar().addListener(SWT.Show, layoutListener);
	editor.getVerticalBar().addListener(SWT.Hide, layoutListener);

	propertyListener = new PropertyChangeListener() {
		public void propertyChange(PropertyChangeEvent evt) {
			String property = evt.getPropertyName();
			if (RulerProvider.PROPERTY_HORIZONTAL_RULER.equals(property)) {
				setRuler((RulerProvider)diagramViewer
						.getProperty(RulerProvider.PROPERTY_HORIZONTAL_RULER), 
					PositionConstants.NORTH);
			} else if (RulerProvider.PROPERTY_VERTICAL_RULER.equals(property)) {
				setRuler((RulerProvider)diagramViewer
						.getProperty(RulerProvider.PROPERTY_VERTICAL_RULER),
					PositionConstants.WEST);
			} else if (RulerProvider.PROPERTY_RULER_VISIBILITY.equals(property))
				setRulerVisibility(((Boolean)diagramViewer.getProperty(
						RulerProvider.PROPERTY_RULER_VISIBILITY)).booleanValue());
		}
	};
	diagramViewer.addPropertyChangeListener(propertyListener);
	Boolean rulerVisibility = (Boolean)diagramViewer
			.getProperty(RulerProvider.PROPERTY_RULER_VISIBILITY);
	if (rulerVisibility != null)
		setRulerVisibility(rulerVisibility.booleanValue());
	setRuler((RulerProvider)diagramViewer.getProperty(
			RulerProvider.PROPERTY_HORIZONTAL_RULER), PositionConstants.NORTH);
	setRuler((RulerProvider)diagramViewer.getProperty(
			RulerProvider.PROPERTY_VERTICAL_RULER), PositionConstants.WEST);
}

private void setRuler(RulerProvider provider, int orientation) {
	Object ruler = null;
	if (isRulerVisible && provider != null)
		// provider.getRuler() might return null (at least the API does not prevent that)
		ruler = provider.getRuler();
	
	if (ruler == null) {
		// Ruler is not visible or is not present
		setRulerContainer(null, orientation);
		// Layout right-away to prevent an empty control from showing
		layout(true);
		return;
	}
	
	GraphicalViewer container = getRulerContainer(orientation);
	if (container == null) {
		container = createRulerContainer(orientation);
		setRulerContainer(container, orientation);
	}
	if (container.getContents() != ruler) {
		container.setContents(ruler);
		needToLayout = true;
		Display.getCurrent().asyncExec(runnable);
	}
}

private void setRulerContainer(GraphicalViewer container, int orientation) {
	if (orientation == PositionConstants.NORTH) {
		if (top == container)
			return;
		disposeRulerViewer(top);
		top = container;		
	} else if (orientation == PositionConstants.WEST) {
		if (left == container)
			return;
		disposeRulerViewer(left);
		left = container;		
	}
}

private void setRulerVisibility(boolean isVisible) {
	if (isRulerVisible != isVisible) {
		isRulerVisible = isVisible;
		if (diagramViewer != null) {
			setRuler((RulerProvider)diagramViewer.getProperty(
					RulerProvider.PROPERTY_HORIZONTAL_RULER), PositionConstants.NORTH);
			setRuler((RulerProvider)diagramViewer.getProperty(
					RulerProvider.PROPERTY_VERTICAL_RULER), PositionConstants.WEST);
		}
	}
}

private static class RulerBorder
	extends AbstractBorder
{
	private static final Insets H_INSETS = new Insets(0, 1, 0, 0);
	private static final Insets V_INSETS = new Insets(1, 0, 0, 0);
	private boolean horizontal;
	/**
	 * Constructor
	 * 
	 * @param isHorizontal	whether or not the ruler being bordered is horizontal or not
	 */
	public RulerBorder(boolean isHorizontal) {
		horizontal = isHorizontal;
	}
	/**
	 * @see com.architexa.org.eclipse.draw2d.Border#getInsets(com.architexa.org.eclipse.draw2d.IFigure)
	 */
	public Insets getInsets(IFigure figure) {
		return horizontal ? H_INSETS : V_INSETS;
	}
	/**
	 * @see com.architexa.org.eclipse.draw2d.Border#paint(com.architexa.org.eclipse.draw2d.IFigure, com.architexa.org.eclipse.draw2d.Graphics, com.architexa.org.eclipse.draw2d.geometry.Insets)
	 */
	public void paint(IFigure figure, Graphics graphics, Insets insets) {
		graphics.setForegroundColor(ColorConstants.buttonDarker);
		if (horizontal) {
			graphics.drawLine(figure.getBounds().getTopLeft(), 
					figure.getBounds().getBottomLeft()
					.translate(new com.architexa.org.eclipse.draw2d.geometry.Point(0, -4)));
		} else {
			graphics.drawLine(figure.getBounds().getTopLeft(), 
					figure.getBounds().getTopRight()
					.translate(new com.architexa.org.eclipse.draw2d.geometry.Point(-4, 0)));
		}
	}
}

/**
 * Custom graphical viewer intended to be used for rulers.
 * 
 * @author Pratik Shah
 * @since 3.0
 */
private static class RulerViewer
	extends ScrollingGraphicalViewer
{
	/**
	 * Constructor
	 */
	public RulerViewer() {
		super();
		init();
	}
	/**
	 * @see com.architexa.org.eclipse.gef.EditPartViewer#appendSelection(com.architexa.org.eclipse.gef.EditPart)
	 */
	public void appendSelection(EditPart editpart) {
		if (editpart instanceof RootEditPart)
			editpart = ((RootEditPart)editpart).getContents();
		setFocus(editpart);
		super.appendSelection(editpart);
	}
	/**
	 * @see com.architexa.org.eclipse.gef.GraphicalViewer#findHandleAt(com.architexa.org.eclipse.draw2d.geometry.Point)
	 */
	public Handle findHandleAt(com.architexa.org.eclipse.draw2d.geometry.Point p) {
		final GraphicalEditPart gep = 
				(GraphicalEditPart)findObjectAtExcluding(p, new ArrayList());
		if (gep == null || !(gep instanceof GuideEditPart))
			return null;
		return new Handle() {
			public DragTracker getDragTracker() {
				return ((GuideEditPart)gep).getDragTracker(null);
			}
			public com.architexa.org.eclipse.draw2d.geometry.Point getAccessibleLocation() {
				return null;
			}
		};
	}
	/**
	 * @see com.architexa.org.eclipse.gef.ui.parts.AbstractEditPartViewer#init()
	 */
	protected void init() {
		setContextMenu(new RulerContextMenuProvider(this));
		setKeyHandler(new RulerKeyHandler(this));
	}
	/**
	 * Requests to reveal a ruler are ignored since that causes undesired scrolling to
	 * the origin of the ruler
	 * 
	 * @see com.architexa.org.eclipse.gef.EditPartViewer#reveal(com.architexa.org.eclipse.gef.EditPart)
	 */
	public void reveal(EditPart part) {
		if (part != getContents())
			super.reveal(part);
	}
	/**
	 * @see com.architexa.org.eclipse.gef.EditPartViewer#setContents(com.architexa.org.eclipse.gef.EditPart)
	 */
	public void setContents(EditPart editpart) {
		super.setContents(editpart);
		setFocus(getContents());
	}
	/**
	 * Custom KeyHandler intended to be used with a RulerViewer
	 * 
	 * @author Pratik Shah
	 * @since 3.0
	 */
	protected static class RulerKeyHandler extends GraphicalViewerKeyHandler {
		/**
		 * Constructor
		 * 
		 * @param viewer	The viewer for which this handler processes keyboard input
		 */
		public RulerKeyHandler(GraphicalViewer viewer) {
			super(viewer);
		}
		/**
		 * @see com.architexa.org.eclipse.gef.KeyHandler#keyPressed(org.eclipse.swt.events.KeyEvent)
		 */
		public boolean keyPressed(KeyEvent event) {
			if (event.keyCode == SWT.DEL) {
				// If a guide has focus, delete it
				if (getFocusEditPart() instanceof GuideEditPart) {
					RulerEditPart parent = 
							(RulerEditPart)getFocusEditPart().getParent();
					getViewer().getEditDomain().getCommandStack().execute(
							parent.getRulerProvider().getDeleteGuideCommand(
							getFocusEditPart().getModel()));
					event.doit = false;
					return true;
				}
				return false;
			} else if (((event.stateMask & SWT.ALT) != 0)
					&& (event.keyCode == SWT.ARROW_UP)) {
				// ALT + UP_ARROW pressed
				// If a guide has focus, give focus to the ruler
				EditPart parent = getFocusEditPart().getParent();
				if (parent instanceof RulerEditPart)
					navigateTo(getFocusEditPart().getParent(), event);
				return true;
			}
			return super.keyPressed(event);
		}
	}
}

}
